﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Xml.Serialization;

namespace DivShare
{
    /// <summary>
    /// Provides method for interacting with DivShare file server.
    /// </summary>
    public class DivShareClient
    {
        #region Private Constants and Fields
        
        /// <summary>
        /// The time in seconds while the file download is being delayed by DivShare server.
        /// </summary>
        private const int WebFileDownloadDelay = 20;

        /// <summary>
        /// The URI of DivShare API.
        /// </summary>
        private readonly Uri ApiUri = new Uri("http://www.divshare.com/api/");
        
        /// <summary>
        /// The URI of DivShare file download page.
        /// </summary>
        private readonly Uri WebFileDownloadPageUri= new Uri("http://www.divshare.com/download/launch/");

        /// <summary>
        /// Regular expression format to extract the direct link to download a DivShare file.
        /// </summary>
        private const string WebFileDownloadPageDirectLinkPatternFormat = @"(?i:(?<DirectLink>http://\w+\.divshare\.com/.+\?f={0}&s={1}))";

        /// <summary>
        /// Regular expression to extract a file name from the download HTTP response.
        /// </summary>
        private const string WebFileDownloadResponseFileNamePattern = "(?i:filename=\"(?<FileName>.+)\")";

        /// <summary>
        /// The URI of DivShare file upload api.
        /// </summary>
        private readonly Uri FileUploadApiUri = new Uri("http://upload.divshare.com/api/upload");

        /// <summary>
        /// The dummy URL where DivShare server sends -in query string- the result of the file upload.
        /// </summary>
        private const string FileUploadResponseUrl = "upload/response/";

        /// <summary>
        /// Regular expression to extract the parts of a webfile id.
        /// </summary>
        private const string WebFileIdPattern = @"^(?i:(?<FileIdPart1>\d+)-(?<FileIdPart2>[a-z0-9]+))$";
        
        #endregion

        #region Events
        
        /// <summary>
        /// Occurs when an asynchronous webfile download operation 
        /// successfully transfers some or all of the data.
        /// </summary>
        /// <remarks>
        /// This event is raised each time an asynchronous webfile download make progress.
        /// This event is raised when downloads are started using any of the following methods:
        /// - DownloadWebFileAsync(string)
        /// - OpenWebFileAsync(string)
        /// </remarks>
        public event EventHandler<DownloadWebFileProgressChangedEventArgs> DownloadWebFileProgressChanged;
        
        /// <summary>
        /// Occurs when an asynchronous webfile download operation completes.
        /// </summary>
        /// <remarks>
        /// This event is raised each time an asynchronous webfile download operation completes. 
        /// Asynchronous webfile downloads are started by calling the DownloadWebFileAsync(string) method.
        /// </remarks>
        public event EventHandler<DownloadWebFileCompletedEventArgs> DownloadWebFileCompleted;

        /// <summary>
        /// Occurs when an asynchronous operation to open a webfile stream containing a resource completes.
        /// </summary>
        /// <remarks>
        /// This event is raised each time an asynchronous operation to open a webfile stream containing a resource completes. 
        /// These operations are started by calling the OpenWebFileAsync(string) method.
        /// </remarks>
        public event EventHandler<OpenWebFileCompletedEventArgs> OpenWebFileCompleted;

        /// <summary>
        /// Occurs when an asynchronous webfile upload operation completes.
        /// </summary>
        /// <remarks>
        /// This event is raised each time an asynchronous webfile upload operation completes. 
        /// Asynchronous webfile uploads are started using any of the following methods:
        /// - UploadWebFileAsync(string, string, string, string)
        /// - UploadWebFileAsync(string, string, string, string, string)
        /// </remarks>
        public event EventHandler<UploadWebFileCompletedEventArgs> UploadWebFileCompleted;

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets a value that determines whether 100-Continue behavior is used when posting data via HTTP.
        /// </summary>
        /// <value><c>true</c> to expect 100-Continue responses for POST requests; otherwise, <c>false</c>. The default value is <c>true</c>.</value>
        public static bool Expect100Continue
        {
            get { return ServicePointManager.Expect100Continue; }
            set { ServicePointManager.Expect100Continue = value; }
        }
        
        /// <summary>
        /// Gets or sets the API key.
        /// </summary>
        /// <value>The API key.</value>
        public string ApiKey { get; private set; }

        /// <summary>
        /// Gets or sets the secret key.
        /// </summary>
        /// <value>The secret key.</value>
        public string SecretKey { get; private set; }

        /// <summary>
        /// Gets or sets the session key.
        /// </summary>
        /// <value>The session key.</value>
        public string SessionKey { get; private set; }

        #endregion

        #region Constructors
        
        /// <summary>
        /// Initializes a new instance of the <see cref="DivShareClient"/> class.
        /// </summary>
        /// <param name="apiKey">The API key.</param>
        /// <param name="secretKey">The secret key.</param>
        public DivShareClient(string apiKey, string secretKey)
            : this (apiKey, secretKey, null)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="DivShareClient"/> class.
        /// </summary>
        /// <param name="apiKey">The API key.</param>
        /// <param name="secretKey">The secret key.</param>
        /// <param name="sessionKey">The session key.</param>
        public DivShareClient(string apiKey, string secretKey, string sessionKey)
        {
            ApiKey = apiKey;
            SecretKey = secretKey;
            SessionKey = sessionKey;

            Expect100Continue = false;
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Logs on to a specified user account and sets the session key appropriatley.
        /// </summary>
        /// <param name="userEmail">The user email.</param>
        /// <param name="userPassword">The user password.</param>
        /// <returns>The session key.</returns>
        /// <exception cref="LogOnFailedException">Thrown when log on request failed.</exception>
        public string LogOn(string userEmail, string userPassword)
        {
            NameValueCollection parameters = new NameValueCollection();
            parameters["user_email"] = userEmail;
            parameters["user_password"] = userPassword;

            LogOnResponse response = CallWebMethod<LogOnResponse>("login", parameters);
            if (response.Success)
            {
                SessionKey = response.SessionKey;
                return response.SessionKey;
            }
            else throw new LogOnFailedException(response.ErrorMessage);
        }

        /// <summary>
        /// Logs off from the current user account and deletes the session key.
        /// </summary>
        /// <returns><c>true</c> if log off was successful; otherwise, <c>false</c>.</returns>
        /// <exception cref="LogOffFailedException">Thrown when log off request failed.</exception>
        public bool LogOff()
        {
            LogOffResponse response = CallWebMethod<LogOffResponse>("logout");
            if (response.Success)
            {
                SessionKey = null;
                return response.IsLoggedOff;
            }
            else throw new LogOffFailedException(response.ErrorMessage);
        }

        /// <summary>
        /// Gets a user info.
        /// </summary>
        /// <returns>The user info.</returns>
        /// <exception cref="OperationFailedException">Thrown when request for user info failed.</exception>
        public UserInfo GetUserInfo()
        {
            GetUserInfoResponse response = CallWebMethod<GetUserInfoResponse>("get_user_info");
            if (response.Success)
                return response.UserInfo;
            else throw new OperationFailedException(response.ErrorMessage);
        }

        /// <summary>
        /// Gets a list of webfiles assigned to the user's account, 
        /// with the most recently uploaded webfiles listed first.
        /// </summary>
        /// <returns>The list of webfiles assigned to the user's account.</returns>
        /// <exception cref="OperationFailedException">Thrown when request for user's webfiles failed.</exception>
        public WebFileInfo[] GetUserWebFiles()
        {
            return GetUserWebFiles(-1, -1);
        }
        /// <summary>
        /// Gets a part of the list of webfiles assigned to the user's account, 
        /// with the most recently uploaded webfiles listed first.
        /// </summary>
        /// <param name="index">The zero-based index of the starting record.</param>
        /// <param name="length">The maximum number of records to return.</param>
        /// <returns>The list of webfiles assigned to the user's account.</returns>
        /// <exception cref="OperationFailedException">Thrown when request for user's webfiles failed.</exception>
        public WebFileInfo[] GetUserWebFiles(int index, int length)
        {
            NameValueCollection parameters = new NameValueCollection();
            if (index >= 0 && length > 0)
            {
                parameters["limit"] = length.ToString(CultureInfo.InvariantCulture);
                parameters["offset"] = index.ToString(CultureInfo.InvariantCulture);
            }

            WebFileInfoListResponse response = CallWebMethod<WebFileInfoListResponse>("get_user_files", parameters);
            if (response.Success)
                return response.WebFileInfos;
            else throw new OperationFailedException(response.ErrorMessage);
        }

        /// <summary>
        /// Gets a list of webfiles in the specified webfolder.
        /// </summary>
        /// <param name="webFolderId">The webfolder id.</param>
        /// <returns>The list of webfiles in the specified webfolder.</returns>
        /// /// <exception cref="OperationFailedException">Thrown when request for webfolder's webfiles failed.</exception>
        public WebFileInfo[] GetWebFolderFiles(string webFolderId)
        {
            return GetWebFolderFiles(webFolderId, -1, -1);
        }
        /// <summary>
        /// Gets a part of the list of webfiles in the specified webfolder.
        /// </summary>
        /// <param name="webFolderId">The webfolder id.</param>
        /// <param name="index">The zero-based index of the starting record.</param>
        /// <param name="length">The maximum number of records to return.</param>
        /// <returns>The list of webfiles in the specified webfolder.</returns>
        /// <exception cref="OperationFailedException">Thrown when request for webfolder's webfiles failed.</exception>
        public WebFileInfo[] GetWebFolderFiles(string webFolderId, int index, int length)
        {
            NameValueCollection parameters = new NameValueCollection();
            parameters["folder_id"] = webFolderId;
            if (index >= 0 && length > 0)
            {
                parameters["limit"] = length.ToString(CultureInfo.InvariantCulture);
                parameters["offset"] = index.ToString(CultureInfo.InvariantCulture);
            }

            WebFileInfoListResponse response = CallWebMethod<WebFileInfoListResponse>("get_folder_files", parameters);
            if (response.Success)
                return response.WebFileInfos;
            else throw new OperationFailedException(response.ErrorMessage);
        }

        /// <summary>
        /// Gets a list of webfiles specified in the list of webfile ids.
        /// </summary>
        /// <param name="webFileIds">The list of webfile ids.</param>
        /// <returns>The list of webfiles specified in the list of webfile ids.</returns>
        /// <exception cref="OperationFailedException">Thrown when request for webfiles failed.</exception>
        public WebFileInfo[] GetWebFiles(params string[] webFileIds)
        {
            NameValueCollection parameters = new NameValueCollection();
            if (webFileIds != null)
                parameters["files"] = String.Join(",", webFileIds);

            WebFileInfoListResponse response = CallWebMethod<WebFileInfoListResponse>("get_files", parameters);
            if (response.Success)
                return response.WebFileInfos;
            else throw new OperationFailedException(response.ErrorMessage);
        }

        /// <summary>
        /// Gets an upload ticket needed to upload a webfile.
        /// </summary>
        /// <returns>The upload ticket.</returns>
        /// <exception cref="OperationFailedException">Thrown when request for upload ticket failed.</exception>
        public string GetWebFileUploadTicket()
        {
            GetUploadTicketResponse response = CallWebMethod<GetUploadTicketResponse>("get_upload_ticket");
            if (response.Success)
                return response.UploadTicket;
            else throw new OperationFailedException(response.ErrorMessage);
        }

        /// <summary>
        /// Downloads a webfile specified by the webfile id.
        /// </summary>
        /// <param name="webFileId">The id of the webfile to download.</param>
        /// <returns>The downloaded webfile.</returns>
        public WebFile DownloadWebFile(string webFileId)
        {
            if (String.IsNullOrEmpty(webFileId))
                throw new ArgumentNullException("webFileId");

            Regex regexFileId = new Regex(WebFileIdPattern);
            if (!regexFileId.IsMatch(webFileId))
                throw new ArgumentOutOfRangeException("webFileId");

            Uri uriDownload = new Uri(WebFileDownloadPageUri.AbsoluteUri + webFileId);
            Match matchFileId = regexFileId.Match(webFileId);
            string directLinkPattern = String.Format(
                CultureInfo.InvariantCulture,
                WebFileDownloadPageDirectLinkPatternFormat,
                matchFileId.Groups["FileIdPart1"].Value,
                matchFileId.Groups["FileIdPart2"].Value);
            Regex regexDirectLink = new Regex(directLinkPattern);
            Regex regexFileName = new Regex(WebFileDownloadResponseFileNamePattern);

            WebClient webClient = new WebClient();
            webClient.Headers["Cookie"] = String.Format(
                CultureInfo.InvariantCulture,
                "divshare_last_file={0}; divshare_last_i={1}",
                matchFileId.Groups["FileIdPart1"].Value,
                UnixDateTimeHelper.Parse(DateTime.Now.AddSeconds(-20)));
            string downloadPage = webClient.DownloadString(uriDownload);
            Uri uriDirectLink = new Uri(regexDirectLink.Match(downloadPage).Groups["DirectLink"].Value);

            WebFile webFile = new WebFile();
            webFile.Content = webClient.DownloadData(uriDirectLink);
            string contentDisposition = webClient.ResponseHeaders["Content-Disposition"];
            webFile.Name = regexFileName.Match(contentDisposition).Groups["FileName"].Value;

            return webFile;
        }

        /// <summary>
        /// Downloads a webfile specified by the webfile id, asynchronously.
        /// </summary>
        /// <param name="webFileId">The id of the webfile to download.</param>
        public void DownloadWebFileAsync(string webFileId)
        {
            if (String.IsNullOrEmpty(webFileId))
                throw new ArgumentNullException("webFileId");

            Regex regexFileId = new Regex(WebFileIdPattern);
            if (!regexFileId.IsMatch(webFileId))
                throw new ArgumentOutOfRangeException("webFileId");

            Uri uriDownload = new Uri(WebFileDownloadPageUri.AbsoluteUri + webFileId);
            Match matchFileId = regexFileId.Match(webFileId);
            string directLinkPattern = String.Format(
                CultureInfo.InvariantCulture,
                WebFileDownloadPageDirectLinkPatternFormat,
                matchFileId.Groups["FileIdPart1"].Value, 
                matchFileId.Groups["FileIdPart2"].Value);
            Regex regexDirectLink = new Regex(directLinkPattern);

            WebClient webClient = new WebClient();
            webClient.Headers["Cookie"] = String.Format(
                CultureInfo.InvariantCulture,
                "divshare_last_file={0}; divshare_last_i={1}",
                matchFileId.Groups["FileIdPart1"].Value, 
                UnixDateTimeHelper.Parse(DateTime.Now.AddSeconds(-WebFileDownloadDelay)));
            string downloadPage = webClient.DownloadString(uriDownload);
            Uri uriDirectLink = new Uri(regexDirectLink.Match(downloadPage).Groups["DirectLink"].Value);

            webClient.DownloadProgressChanged += (sender, e) =>
            {
                OnDownloadWebFileProgressChanged(
                    new DownloadWebFileProgressChangedEventArgs(
                        e.BytesReceived,
                        e.TotalBytesToReceive,
                        e.ProgressPercentage));
            };
            webClient.DownloadDataCompleted += (sender, e) =>
            {
                WebFile webFile = null;
                if (e.Error == null)
                {
                    Regex regexFileName = new Regex(WebFileDownloadResponseFileNamePattern);

                    webFile = new WebFile();
                    webFile.Content = e.Result;
                    string contentDisposition = ((WebClient)sender).ResponseHeaders["Content-Disposition"];
                    webFile.Name = regexFileName.Match(contentDisposition).Groups["FileName"].Value;
                }

                OnDownloadWebFileCompleted(
                    new DownloadWebFileCompletedEventArgs(
                        webFile,
                        e.Error));
            };
            webClient.DownloadDataAsync(uriDirectLink);
        }

        /// <summary>
        /// Opens a webfile specified by the webfile id.
        /// </summary>
        /// <param name="webFileId">The id if the webfile to open.</param>
        /// <returns>The readable stream that contains the webfile's content.</returns>
        public Stream OpenWebFile(string webFileId)
        {
            if (String.IsNullOrEmpty(webFileId))
                throw new ArgumentNullException("webFileId");
            
            Regex regexFileId = new Regex(WebFileIdPattern);
            if (!regexFileId.IsMatch(webFileId))
                throw new ArgumentOutOfRangeException("webFileId");

            Uri uriDownload = new Uri(WebFileDownloadPageUri.AbsoluteUri + webFileId);
            Match matchFileId = regexFileId.Match(webFileId);
            string directLinkPattern = String.Format(
                CultureInfo.InvariantCulture,
                WebFileDownloadPageDirectLinkPatternFormat,
                matchFileId.Groups["FileIdPart1"].Value,
                matchFileId.Groups["FileIdPart2"].Value);
            Regex regexDirectLink = new Regex(directLinkPattern);

            WebClient webClient = new WebClient();
            webClient.Headers["Cookie"] = String.Format(
                CultureInfo.InvariantCulture,
                "divshare_last_file={0}; divshare_last_i={1}",
                matchFileId.Groups["FileIdPart1"].Value,
                UnixDateTimeHelper.Parse(DateTime.Now.AddSeconds(-WebFileDownloadDelay)));
            string downloadPage = webClient.DownloadString(uriDownload);
            Uri uriDirectLink = new Uri(regexDirectLink.Match(downloadPage).Groups["DirectLink"].Value);

            Stream wfStream = webClient.OpenRead(uriDirectLink);
            return wfStream;
        }

        /// <summary>
        /// Opens a webfile specified by the webfile id, asynchronously.
        /// </summary>
        /// <param name="webFileId">The id of the webfile to open.</param>
        public void OpenWebFileAsync(string webFileId)
        {
            if (String.IsNullOrEmpty(webFileId))
                throw new ArgumentNullException("webFileId");

            Regex regexFileId = new Regex(WebFileIdPattern);
            if (!regexFileId.IsMatch(webFileId))
                throw new ArgumentOutOfRangeException("webFileId");

            Uri uriDownload = new Uri(WebFileDownloadPageUri.AbsoluteUri + webFileId);
            Match matchFileId = regexFileId.Match(webFileId);
            string directLinkPattern = String.Format(
                CultureInfo.InvariantCulture,
                WebFileDownloadPageDirectLinkPatternFormat,
                matchFileId.Groups["FileIdPart1"].Value,
                matchFileId.Groups["FileIdPart2"].Value);
            Regex regexDirectLink = new Regex(directLinkPattern);

            WebClient webClient = new WebClient();
            webClient.Headers["Cookie"] = String.Format(
                CultureInfo.InvariantCulture,
                "divshare_last_file={0}; divshare_last_i={1}",
                matchFileId.Groups["FileIdPart1"].Value,
                UnixDateTimeHelper.Parse(DateTime.Now.AddSeconds(-WebFileDownloadDelay)));
            string downloadPage = webClient.DownloadString(uriDownload);
            Uri uriDirectLink = new Uri(regexDirectLink.Match(downloadPage).Groups["DirectLink"].Value);

            webClient.DownloadProgressChanged += (sender, e) =>
            {
                OnDownloadWebFileProgressChanged(
                    new DownloadWebFileProgressChangedEventArgs(
                        e.BytesReceived,
                        e.TotalBytesToReceive,
                        e.ProgressPercentage));
            };
            webClient.OpenReadCompleted += (sender, e) =>
            {
                Stream wfStream = e.Result;
                OnOpenWebFileCompleted(
                    new OpenWebFileCompletedEventArgs(
                        wfStream,
                        e.Error));
            };

            webClient.OpenReadAsync(uriDirectLink);
        }

        /// <summary>
        /// Uploads a webfile to the specified webfolder.
        /// </summary>
        /// <param name="webFileName">Name of the webfile (inclusing path) to upload.</param>
        /// <param name="webFileDescription">Description of the webfile to upload.</param>
        /// <param name="webFolderId">The id of the webfolder where webfile needs to be uploaded. To upload into the root webfolder, leave it empty.</param>
        /// <param name="uploadTicket">The upload ticket.</param>
        /// <returns>The id of the webfile being uploaded.</returns>
        public string UploadWebFile(string webFileName, string webFileDescription, string webFolderId, string uploadTicket)
        {
            return UploadWebFile(webFileName, webFileDescription, webFolderId, uploadTicket, null);
        }

        /// <summary>
        /// Uploads a webfile to the specified webfolder and DivShare server sends a notification to the specified email address.
        /// </summary>
        /// <param name="webFileName">Name of the webfile (inclusing path) to upload.</param>
        /// <param name="webFileDescription">Description of the webfile to upload.</param>
        /// <param name="webFolderId">The id of the webfolder where webfile needs to be uploaded. To upload into the root webfolder, leave it empty.</param>
        /// <param name="uploadTicket">The upload ticket.</param>
        /// <param name="emailTo">The email address where DivShare server can send the notification about the successful webfile upload.</param>
        /// <returns>The id of the webfile being uploaded.</returns>
        public string UploadWebFile(string webFileName, string webFileDescription, string webFolderId, string uploadTicket, string emailTo)
        {
            if (String.IsNullOrEmpty(webFileName))
                throw new ArgumentNullException("webFileName");
            if (String.IsNullOrEmpty(uploadTicket))
                throw new ArgumentNullException("uploadTicket");

            FileInfo fiWebFile = new FileInfo(webFileName);
            if (!fiWebFile.Exists)
                throw new FileNotFoundException("WebFile to upload cannot be found!", fiWebFile.FullName);

            
            string mpBoundaryString = String.Format(
                CultureInfo.InvariantCulture,
                "---------------------------{0:x}",
                DateTime.Now.Ticks);
            byte[] mpLastBoundaryBytes = Encoding.ASCII.GetBytes(
                String.Format(CultureInfo.InvariantCulture, "{1}--{0}--{1}", mpBoundaryString, Environment.NewLine));
            
            List<byte[]> lMultiPartFormHeaderBytes = new List<byte[]>(6);
            lMultiPartFormHeaderBytes.Add(GenerateMultiPartFormFieldHeaderBytes("upload_ticket", mpBoundaryString));
            lMultiPartFormHeaderBytes.Add(GenerateMultiPartFormFieldHeaderBytes("response_url", mpBoundaryString));
            lMultiPartFormHeaderBytes.Add(GenerateMultiPartFormFieldHeaderBytes("email_to", mpBoundaryString));
            lMultiPartFormHeaderBytes.Add(GenerateMultiPartFormFieldHeaderBytes("folder_id", mpBoundaryString));
            lMultiPartFormHeaderBytes.Add(GenerateMultiPartFormFieldHeaderBytes("file1_description", mpBoundaryString));
            lMultiPartFormHeaderBytes.Add(GenerateMultiPartFormFieldHeaderBytes("file1", fiWebFile.Name, mpBoundaryString));

            List<byte[]> lMultiPartFormBodyBytes = new List<byte[]>(5);
            lMultiPartFormBodyBytes.Add(GenerateMultiPartFormFieldContentBytes(uploadTicket));
            lMultiPartFormBodyBytes.Add(GenerateMultiPartFormFieldContentBytes(FileUploadResponseUrl));
            lMultiPartFormBodyBytes.Add(GenerateMultiPartFormFieldContentBytes(emailTo));
            lMultiPartFormBodyBytes.Add(GenerateMultiPartFormFieldContentBytes(webFolderId));
            lMultiPartFormBodyBytes.Add(GenerateMultiPartFormFieldContentBytes(webFileDescription));

            long mpFormHeaderLength = 0;
            foreach (byte[] mpFormHeaderBytes in lMultiPartFormHeaderBytes)
            {
                int mpLength = mpFormHeaderBytes.Length;
                mpFormHeaderLength += mpLength;
            }

            long mpFormBodyLength = 0;
            foreach (byte[] mpFormBodyBytes in lMultiPartFormBodyBytes)
            {
                int mpLength = mpFormBodyBytes.Length;
                mpFormBodyLength += mpLength;
            }
            mpFormBodyLength += fiWebFile.Length;
            
            HttpWebRequest webreq = (HttpWebRequest)WebRequest.Create(FileUploadApiUri);
            webreq.AllowAutoRedirect = false;
            webreq.ContentType = String.Format(
                CultureInfo.InvariantCulture,
                "multipart/form-data; boundary=\"{0}\"",
                mpBoundaryString);
            webreq.ContentLength = mpFormHeaderLength + mpFormBodyLength + mpLastBoundaryBytes.Length;
            webreq.Method = WebRequestMethods.Http.Post;

            using (Stream sw = webreq.GetRequestStream())
            {
                for (int idxForm = 0; idxForm < lMultiPartFormBodyBytes.Count; idxForm++)
                {
                    byte[] mpFormHeaderBytes = lMultiPartFormHeaderBytes[idxForm];
                    byte[] mpFormBodyBytes = lMultiPartFormBodyBytes[idxForm];

                    sw.Write(mpFormHeaderBytes, 0, mpFormHeaderBytes.Length);
                    sw.Write(mpFormBodyBytes, 0, mpFormBodyBytes.Length);
                }

                byte[] mpFormHeaderFileUploadBytes = lMultiPartFormHeaderBytes[5];
                sw.Write(mpFormHeaderFileUploadBytes, 0, mpFormHeaderFileUploadBytes.Length);

                using (FileStream fs = fiWebFile.OpenRead())
                {
                    int count = 0;
                    byte[] buffer = new byte[64 * 1024];
                    while ((count = fs.Read(buffer, 0, buffer.Length)) > 0)
                        sw.Write(buffer, 0, count);
                }

                sw.Write(mpLastBoundaryBytes, 0, mpLastBoundaryBytes.Length);
            }

            WebResponse webresp = webreq.GetResponse();
            Uri uriResponse = new Uri(webresp.Headers["Location"]);
            NameValueCollection nvcResponse = HttpUtility.ParseQueryString(uriResponse.Query);

            if (!String.IsNullOrEmpty(nvcResponse["error"]))
            {
                string message = nvcResponse["description"];
                throw new OperationFailedException(message);
            }
            
            string webFileId = nvcResponse["file1"];
            return webFileId;
        }

        /// <summary>
        /// Uploads a webfile to the specified webfolder, asynchronously.
        /// </summary>
        /// <param name="webFileName">Name of the webfile (inclusing path) to upload.</param>
        /// <param name="webFileDescription">Description of the webfile to upload.</param>
        /// <param name="webFolderId">The id of the webfolder where webfile needs to be uploaded. To upload into the root webfolder, leave it empty.</param>
        /// <param name="uploadTicket">The upload ticket.</param>
        public void UploadWebFileAsync(string webFileName, string webFileDescription, string webFolderId, string uploadTicket)
        {
            UploadWebFileAsync(webFileName, webFileDescription, webFolderId, uploadTicket, null);
        }
        
        /// <summary>
        /// Uploads a webfile to the specified webfolder, asynchronously and DivShare server sends a notification to the specified email address.
        /// </summary>
        /// <param name="webFileName">Name of the webfile (inclusing path) to upload.</param>
        /// <param name="webFileDescription">Description of the webfile to upload.</param>
        /// <param name="webFolderId">The id of the webfolder where webfile needs to be uploaded. To upload into the root webfolder, leave it empty.</param>
        /// <param name="uploadTicket">The upload ticket.</param>
        /// <param name="emailTo">The email address where DivShare server can send the notification about the successful file upload.</param>
        public void UploadWebFileAsync(string webFileName, string webFileDescription, string webFolderId, string uploadTicket, string emailTo)
        {
            if (String.IsNullOrEmpty(webFileName))
                throw new ArgumentNullException("webFileName");
            if (String.IsNullOrEmpty(uploadTicket))
                throw new ArgumentNullException("uploadTicket");

            FileInfo fiWebFile = new FileInfo(webFileName);
            if (!fiWebFile.Exists)
                throw new FileNotFoundException("WebFile to upload cannot be found!", fiWebFile.FullName);


            string mpBoundaryString = String.Format(
                CultureInfo.InvariantCulture,
                "---------------------------{0:x}",
                DateTime.Now.Ticks);
            byte[] mpLastBoundaryBytes = Encoding.ASCII.GetBytes(
                String.Format(CultureInfo.InvariantCulture, "{1}--{0}--{1}", mpBoundaryString, Environment.NewLine));

            List<byte[]> lMultiPartFormHeaderBytes = new List<byte[]>(6);
            lMultiPartFormHeaderBytes.Add(GenerateMultiPartFormFieldHeaderBytes("upload_ticket", mpBoundaryString));
            lMultiPartFormHeaderBytes.Add(GenerateMultiPartFormFieldHeaderBytes("response_url", mpBoundaryString));
            lMultiPartFormHeaderBytes.Add(GenerateMultiPartFormFieldHeaderBytes("email_to", mpBoundaryString));
            lMultiPartFormHeaderBytes.Add(GenerateMultiPartFormFieldHeaderBytes("folder_id", mpBoundaryString));
            lMultiPartFormHeaderBytes.Add(GenerateMultiPartFormFieldHeaderBytes("file1_description", mpBoundaryString));
            lMultiPartFormHeaderBytes.Add(GenerateMultiPartFormFieldHeaderBytes("file1", fiWebFile.Name, mpBoundaryString));

            List<byte[]> lMultiPartFormBodyBytes = new List<byte[]>(5);
            lMultiPartFormBodyBytes.Add(GenerateMultiPartFormFieldContentBytes(uploadTicket));
            lMultiPartFormBodyBytes.Add(GenerateMultiPartFormFieldContentBytes(FileUploadResponseUrl));
            lMultiPartFormBodyBytes.Add(GenerateMultiPartFormFieldContentBytes(emailTo));
            lMultiPartFormBodyBytes.Add(GenerateMultiPartFormFieldContentBytes(webFolderId));
            lMultiPartFormBodyBytes.Add(GenerateMultiPartFormFieldContentBytes(webFileDescription));

            long mpFormHeaderLength = 0;
            foreach (byte[] mpFormHeaderBytes in lMultiPartFormHeaderBytes)
            {
                int mpLength = mpFormHeaderBytes.Length;
                mpFormHeaderLength += mpLength;
            }

            long mpFormBodyLength = 0;
            foreach (byte[] mpFormBodyBytes in lMultiPartFormBodyBytes)
            {
                int mpLength = mpFormBodyBytes.Length;
                mpFormBodyLength += mpLength;
            }
            mpFormBodyLength += fiWebFile.Length;

            HttpWebRequest webreq = (HttpWebRequest)WebRequest.Create(FileUploadApiUri);
            webreq.AllowAutoRedirect = false;
            webreq.ContentType = String.Format(
                CultureInfo.InvariantCulture,
                "multipart/form-data; boundary=\"{0}\"",
                mpBoundaryString);
            webreq.ContentLength = mpFormHeaderLength + mpFormBodyLength + mpLastBoundaryBytes.Length;
            webreq.Method = WebRequestMethods.Http.Post;

            webreq.BeginGetRequestStream(delegate(IAsyncResult arGetRequestStream)
            {
                using (Stream sw = webreq.EndGetRequestStream(arGetRequestStream))
                {
                    for (int idxForm = 0; idxForm < lMultiPartFormBodyBytes.Count; idxForm++)
                    {
                        byte[] mpFormHeaderBytes = lMultiPartFormHeaderBytes[idxForm];
                        byte[] mpFormBodyBytes = lMultiPartFormBodyBytes[idxForm];

                        sw.Write(mpFormHeaderBytes, 0, mpFormHeaderBytes.Length);
                        sw.Write(mpFormBodyBytes, 0, mpFormBodyBytes.Length);
                    }

                    byte[] mpFormHeaderFileUploadBytes = lMultiPartFormHeaderBytes[5];
                    sw.Write(mpFormHeaderFileUploadBytes, 0, mpFormHeaderFileUploadBytes.Length);

                    using (FileStream fs = fiWebFile.OpenRead())
                    {
                        int count = 0;
                        byte[] buffer = new byte[64 * 1024];
                        while ((count = fs.Read(buffer, 0, buffer.Length)) > 0)
                            sw.Write(buffer, 0, count);
                    }

                    sw.Write(mpLastBoundaryBytes, 0, mpLastBoundaryBytes.Length);
                }

                webreq.BeginGetResponse(delegate(IAsyncResult arGetResponseStream)
                {
                    WebResponse webresp = webreq.EndGetResponse(arGetResponseStream);
                    Uri uriResponse = new Uri(webresp.Headers["Location"]);
                    NameValueCollection nvcResponse = HttpUtility.ParseQueryString(uriResponse.Query);

                    if (!String.IsNullOrEmpty(nvcResponse["error"]))
                    {
                        string message = nvcResponse["description"];
                        OnUploadWebFileCompleted(
                            new UploadWebFileCompletedEventArgs(
                                null,
                                new OperationFailedException(message)));
                    }
                    else
                    {
                        string webFileId = nvcResponse["file1"];
                        OnUploadWebFileCompleted(
                            new UploadWebFileCompletedEventArgs(
                                webFileId,
                                null));
                    }
                },
                null);
            },
            null);
        }
        #endregion

        #region Protected Methods

        /// <summary>
        /// Raises the <see cref="E:DownloadWebFileProgressChanged"/> event.
        /// </summary>
        /// <param name="e">The <see cref="DivShare.DownloadWebFileProgressChangedEventArgs"/> instance containing the event data.</param>
        protected void OnDownloadWebFileProgressChanged(DownloadWebFileProgressChangedEventArgs e)
        {
            if (DownloadWebFileProgressChanged != null)
                DownloadWebFileProgressChanged(this, e);
        }

        /// <summary>
        /// Raises the <see cref="E:DownloadWebFileCompleted"/> event.
        /// </summary>
        /// <param name="e">The <see cref="DivShare.DownloadWebFileCompletedEventArgs"/> instance containing the event data.</param>
        protected void OnDownloadWebFileCompleted(DownloadWebFileCompletedEventArgs e)
        {
            if (DownloadWebFileCompleted != null)
                DownloadWebFileCompleted(this, e);
        }

        /// <summary>
        /// Raises the <see cref="E:OpenWebFileCompleted"/> event.
        /// </summary>
        /// <param name="e">The <see cref="DivShare.OpenWebFileCompletedEventArgs"/> instance containing the event data.</param>
        protected void OnOpenWebFileCompleted(OpenWebFileCompletedEventArgs e)
        {
            if (OpenWebFileCompleted != null)
                OpenWebFileCompleted(this, e);
        }

        /// <summary>
        /// Raises the <see cref="E:UploadWebFileCompleted"/> event.
        /// </summary>
        /// <param name="e">The <see cref="DivShare.UploadWebFileCompletedEventArgs"/> instance containing the event data.</param>
        protected void OnUploadWebFileCompleted(UploadWebFileCompletedEventArgs e)
        {
            if (UploadWebFileCompleted != null)
                UploadWebFileCompleted(this, e);
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Generates the header bytes of a form field for a multi part request.
        /// </summary>
        /// <param name="fieldName">The name of the field.</param>
        /// <param name="multipartBoundary">The boundary of the multi part request.</param>
        /// <returns>The list of bytes representing the form field's header.</returns>
        private static byte[] GenerateMultiPartFormFieldHeaderBytes(string fieldName, string multipartBoundary)
        {
            StringBuilder sb = new StringBuilder();
            if (!String.IsNullOrEmpty(fieldName))
            {
                sb.AppendLine(String.Format(CultureInfo.InvariantCulture, "--{0}", multipartBoundary));
                sb.AppendLine(String.Format(CultureInfo.InvariantCulture, "Content-Disposition: form-data; name=\"{0}\"", fieldName));
                sb.AppendLine();
            }
            string mpFormHeaderString = sb.ToString();
            byte[] mpFormHeaderBytes = Encoding.UTF8.GetBytes(mpFormHeaderString);
            return mpFormHeaderBytes;
        }

        /// <summary>
        /// Generates the header bytes of a form file field for a multi part request.
        /// </summary>
        /// <param name="fieldName">The name of the field.</param>
        /// <param name="fileName">The name of the file.</param>
        /// <param name="multipartBoundary">The boundary of the multi part request.</param>
        /// <returns>The list of bytes representing the form file field's header.</returns>
        private static byte[] GenerateMultiPartFormFieldHeaderBytes(string fieldName, string fileName, string multipartBoundary)
        {
            StringBuilder sb = new StringBuilder();
            if (!String.IsNullOrEmpty(fieldName))
            {
                sb.AppendLine(String.Format(CultureInfo.InvariantCulture, "--{0}", multipartBoundary));
                sb.AppendLine(String.Format(CultureInfo.InvariantCulture, "Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"", fieldName, fileName));
                sb.AppendLine(String.Format(CultureInfo.InvariantCulture, "Content-Type: {0}", MimeTypeHelper.GetContentType(fileName)));
                sb.AppendLine();
            }
            string mpFormHeaderString = sb.ToString();
            byte[] mpFormHeaderBytes = Encoding.UTF8.GetBytes(mpFormHeaderString);
            return mpFormHeaderBytes;
        }

        /// <summary>
        /// Generates the content bytes of a form field for a multi part request.
        /// </summary>
        /// <param name="fieldValue">The value of the field.</param>
        /// <returns>The list of bytes representing the form field's content.</returns>
        private static byte[] GenerateMultiPartFormFieldContentBytes(string fieldValue)
        {
            string mpFormBodyString = String.Format(CultureInfo.InvariantCulture, "{0}{1}", fieldValue, Environment.NewLine);
            byte[] mpFormBodyBytes = Encoding.UTF8.GetBytes(mpFormBodyString);
            return mpFormBodyBytes;
        }

        /// <summary>
        /// Generates the signature for the webmethod call.
        /// </summary>
        /// <param name="parameters">The list of parameters used in the webmethod call.</param>
        /// <returns>The webmethod signature.</returns>
        protected string GenerateWebMethodSignature(NameValueCollection parameters)
        {
            StringBuilder sbParameters = new StringBuilder();
            sbParameters.Append(SecretKey);
            sbParameters.Append(SessionKey);

            if (parameters != null && parameters.Count > 0)
            {
                SortedList<string, string> slParameter = new SortedList<string, string>();
                foreach (string name in parameters.AllKeys)
                    slParameter.Add(name, parameters[name]);
                
                foreach (var parameter in slParameter)
                {
                    sbParameters.Append(parameter.Key);
                    sbParameters.Append(parameter.Value);
                }
            }

            MD5 md5 = new MD5CryptoServiceProvider();
            byte[] buffer = Encoding.ASCII.GetBytes(sbParameters.ToString());
            byte[] hash = md5.ComputeHash(buffer);

            StringBuilder sbHash = new StringBuilder();
            foreach (byte b in hash)
                sbHash.Append(b.ToString("x2", CultureInfo.InvariantCulture));

            return sbHash.ToString();
        }

        /// <summary>
        /// Calls a webmethod without any additional parameters.
        /// </summary>
        /// <typeparam name="TResponse">The type of the webmethod's response.</typeparam>
        /// <param name="webMethodName">Name of the webmethod to call.</param>
        /// <returns>The strongly-typed response of the webmethod call.</returns>
        protected TResponse CallWebMethod<TResponse>(string webMethodName)
            where TResponse : GeneralResponse
        {
            return CallWebMethod<TResponse>(webMethodName, null);
        }
        /// <summary>
        /// Calls a webmethod with additional parameters. 
        /// </summary>
        /// <typeparam name="TResponse">The type of the webmethod's response.</typeparam>
        /// <param name="webMethodName">Name of the webmethod to call.</param>
        /// <param name="parameters">The parameters to pass to the webmethod call.</param>
        /// <returns>The strongly-typed response of the webmethod call.</returns>
        protected TResponse CallWebMethod<TResponse>(string webMethodName, NameValueCollection parameters)
            where TResponse : GeneralResponse
        {
            NameValueCollection innerParameters = new NameValueCollection();
            innerParameters["method"] = webMethodName;
            innerParameters["api_key"] = ApiKey;
            if (!String.IsNullOrEmpty(SessionKey))
            {
                innerParameters["api_session_key"] = SessionKey;
                innerParameters["api_sig"] = GenerateWebMethodSignature(parameters);
            }
            if (parameters != null)
                foreach (string name in parameters.AllKeys)
                    innerParameters.Add(name, parameters[name]);

            WebClient webClient = new WebClient();
            byte[] postResponse = webClient.UploadValues(
                ApiUri,
                WebRequestMethods.Http.Post,
                innerParameters);
            
            TResponse response = null;
            using (MemoryStream ms = new MemoryStream(postResponse))
            {
                XmlSerializer xs = new XmlSerializer(typeof(TResponse));
                response = xs.Deserialize(ms) as TResponse;
            }
            return response;
        }
        #endregion
    }
}
